package com.gitee.wsl.jvm.ext.store


import com.gitee.wsl.ext.array.encodeBase64ToString
import com.gitee.wsl.jvm.ext.base.UTF8
import com.gitee.wsl.jvm.format.FormatExt
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okio.ByteString.Companion.encodeUtf8
import okio.HashingSink
import okio.blackholeSink
import okio.buffer
import okio.source
import timber.log.Timber
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.BufferedWriter
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.FileWriter
import java.io.IOException
import java.io.PrintWriter
import java.io.RandomAccessFile
import java.net.URLConnection
import java.nio.channels.FileLock
import java.text.Collator
import java.util.Collections
import java.util.Locale


inline val fileSeparator: String get() = File.separator

object FileExt{

    //根据自定义对象 文件名排序 （文件夹在上面，文件在下面）
    fun sortFilesByInfo(fileList: List<File>) {
        Collections.sort(fileList, java.util.Comparator<File> { o1: File, o2: File ->
            return@Comparator if (o1.isDirectory && !o2.isDirectory)  -1
            else if (!o1.isDirectory && o2.isDirectory)  1
            else Collator.getInstance(Locale.CHINA).compare(o1.name, o2.name)
        })
    }

    //获取文件夹下面的文件数量
    fun getSubfolderNum(path: String): Int {
        var i = 0
        val files = File(path).listFiles() ?: return -1
        for (f in files) {
            if (f.name.indexOf(".") != 0) {
                i++
            }
        }
        return i
    }

    fun cutFile(
        file: File,
        currentBlock: Int,
        //基础的裁剪大小20M
        baseCuttingSize:Int = 20 * 1024 * 1024,
        //总的块数
        sumBlock:Int = file.sumCuttingBlock(baseCuttingSize)
    ): ByteArray? {
        Timber.d( "getBlockThree:000000---$currentBlock")
        val size = 20 * 1024 * 1024
        var endResult: ByteArray? = null
        val result = ByteArray(size)
        val out = ByteArrayOutputStream()
        try {
            val accessFile = RandomAccessFile(file, "rw")
            accessFile.seek((currentBlock * size).toLong())
            val currentPosition = file.length() % baseCuttingSize
            //判断是否整除
            endResult = if (currentPosition > 0L) {
                //当前的位数和总数是否相等（是不是最后一段）
                if (currentBlock + 1 != sumBlock) {
                    val len = accessFile.read(result)
                    out.write(result, 0, len)
                    out.toByteArray()
                } else {
                    //当有余数时
                    //当前位置2147483647-20971520
                    val bytes = ByteArray(currentPosition.toInt())
                    val len = accessFile.read(bytes)
                    out.write(bytes, 0, len)
                    out.toByteArray()
                }
            } else {
                val len = accessFile.read(result)
                out.write(result, 0, len)
                out.toByteArray()
            }
            accessFile.close()
            out.close()
        } catch (e: IOException) {
            Timber.d(e)
            endResult = null
        }
        return endResult
    }

    suspend fun readFile(
        filePath: String,
        onSuccess: (ByteArray) -> Unit,
        onFailure: (IOException) -> Unit
    ) = withContext(Dispatchers.IO) {
        try {
            BufferedInputStream(FileInputStream(filePath)).use { inputStream ->
                val data = inputStream.readBytes()
                onSuccess(data)
            }
        } catch (e: IOException) {
            onFailure(e)
        }
    }

    suspend fun writeFile(
        filePath: String,
        data: ByteArray,
        onSuccess: () -> Unit,
        onFailure: (IOException) -> Unit
    ) = withContext(Dispatchers.IO) {
        try {
            BufferedOutputStream(FileOutputStream(filePath)).use { outputStream ->
                outputStream.write(data)
            }
            onSuccess()
        } catch (e: IOException) {
            onFailure(e)
        }
    }

    suspend fun readLargeFile(
        filePath: String,
        chunkSize: Int = 4096,
        onChunkRead: (ByteArray, Int) -> Unit,
        onComplete: () -> Unit,
        onFailure: (IOException) -> Unit
    ) = withContext(Dispatchers.IO) {
        try {
            BufferedInputStream(FileInputStream(filePath)).use { inputStream ->
                val buffer = ByteArray(chunkSize)
                var bytesRead: Int
                while (inputStream.read(buffer).also { bytesRead = it } != -1) {
                    onChunkRead(buffer, bytesRead)
                }
            }
            onComplete()
        } catch (e: IOException) {
            onFailure(e)
        }
    }

    suspend fun writeLargeFile(
        filePath: String,
        data: ByteArray,
        chunkSize: Int = 4096,
        onSuccess: () -> Unit,
        onFailure: (IOException) -> Unit
    ) = withContext(Dispatchers.IO) {
        try {
            FileOutputStream(filePath).use { fos ->
                data.inputStream().use { inputStream ->
                    val buffer = ByteArray(chunkSize)
                    var read: Int
                    while (inputStream.read(buffer).also { read = it } != -1) {
                        fos.write(buffer, 0, read)
                    }
                }
            }
            onSuccess()
        } catch (e: IOException) {
            onFailure(e)
        }
    }

}

operator fun File.div(child: String): File = File(this, child)


fun File.sumCuttingBlock(baseCuttingSize:Int = 20 * 1024 * 1024):Int{
    var sumBlock: Long = length() / baseCuttingSize
    val currentPosition = length() % baseCuttingSize
    if (currentPosition > 0L) {
        sumBlock += 1
    }
    return sumBlock.toInt()
}

fun File.createAndWrite(content: String):Boolean{
    if(!createNewFileAfterDeleteExist())
        return false
    writeText(content, UTF8)
    return true
}


inline fun File.print(crossinline block: PrintWriter.() -> Unit) =
    PrintWriter(BufferedWriter(FileWriter(this))).apply(block).close()

inline val File.mimeType: String? get() = URLConnection.guessContentTypeFromName(name)

fun File.requireFileExist(){
    if (!exists()) throw IOException("$this doesn't exist.")
}

fun File.requireFileCreate() {
    if (exists()) throw IOException("$this already exists.")
}

fun File.isExistOrCreateNewFile(): Boolean =
    try {
        if (exists()) {
            isFile
        } else {
            parentFile.isExistOrCreateNewDir() && createNewFile()
        }
    } catch (e: IOException) {
        e.printStackTrace()
        false
    }

fun File?.isExistOrCreateNewDir(): Boolean =
    when{
        this == null ->false
        exists() -> isDirectory
        else ->mkdir()
    }

fun File.getDirectChildrenCount(countHiddenItems: Boolean): Int {
    return listFiles()?.filter {
        if (countHiddenItems) {
            true
        } else {
            !it.name.startsWith('.')
        }
    }?.size ?: 0
}

fun File.newName(): String {
    var index = 1
    var candidate: String
    val split = nameWithoutExtension.split(' ').toMutableList()
    val last = split.last()
    if ("""^\(\d+\)$""".toRegex().matches(last)) {
        split.removeLast()
    }
    val name = split.joinToString(" ")
    while (true) {
        candidate = if (extension.isEmpty()) "$name ($index)" else "$name ($index).$extension"
        if (!File("$parent/$candidate").exists()) {
            return candidate
        }
        index++
    }
}

fun File.ensureParentDirExists(): Boolean {
    val parent = this.parentFile?:return false
    return if (!parent.exists()) parent.mkdirs() else parent.isDirectory

}

suspend fun File.readBytesAsync(): ByteArray? = withContext(Dispatchers.IO) {
    try {
        this@readBytesAsync.inputStream().use { it.readBytes() }
    } catch (e: IOException) {
        println("Error reading file asynchronously: ${e.message}")
        null
    }
}


fun File.newPath(): String {
    return "$parent/" + newName()
}

fun File.createNewFileAfterDeleteExist(): Boolean =
    try {
        if (exists()) {
            delete() && parentFile.isExistOrCreateNewDir() && createNewFile()
        } else {
            parentFile.isExistOrCreateNewDir() && createNewFile()
        }
    } catch (e: IOException) {
        e.printStackTrace()
        false
    }

fun File.rename(newName: String): Boolean =
    exists() && newName.isNotEmpty() && name != newName &&
            File("${parent.orEmpty()}$fileSeparator$newName").let { renameFile ->
                !renameFile.exists() && renameTo(renameFile)
            }

val File.formatFileSize:String
    get() = FormatExt.formatFileSize(length())

val File.dirSize:Long
    get() {
        var size=0L
        listFiles()?.forEach { file->
            size += if(file.isDirectory) file.dirSize else file.length()
        }
        return size
    }

val File.formatDirSize:String
    get() = FormatExt.formatFileSize(dirSize)


fun File.checkMD5(md5: String): Boolean = calculateMD5().equals(md5, true)

fun File.checkSHA1(sha1: String): Boolean = calculateSHA1().equals(sha1, true)

fun File.checkSHA256(sha256: String): Boolean = calculateSHA256().equals(sha256, true)

fun File.checkSHA512(sha512: String): Boolean = calculateSHA512().equals(sha512, true)

fun File.checkHmacSHA1(key: String, hmacSHA1: String): Boolean =
    calculateHmacSHA1(key).equals(hmacSHA1, true)

fun File.checkHmacSHA256(key: String, hmacSHA256: String): Boolean =
    calculateHmacSHA256(key).equals(hmacSHA256, true)

fun File.checkHmacSHA512(key: String, hmacSHA512: String): Boolean =
    calculateHmacSHA512(key).equals(hmacSHA512, true)

fun File.calculateMD5(): String = calculateHash(HashingSink.md5(blackholeSink()))

fun File.calculateSHA1(): String = calculateHash(HashingSink.sha1(blackholeSink()))

fun File.calculateSHA256(): String = calculateHash(HashingSink.sha256(blackholeSink()))

fun File.calculateSHA512(): String = calculateHash(HashingSink.sha512(blackholeSink()))

fun File.calculateHmacSHA1(key: String): String =
    calculateHash(HashingSink.hmacSha1(blackholeSink(), key.encodeUtf8()))

fun File.calculateHmacSHA256(key: String): String =
    calculateHash(HashingSink.hmacSha256(blackholeSink(), key.encodeUtf8()))

fun File.calculateHmacSHA512(key: String): String =
    calculateHash(HashingSink.hmacSha512(blackholeSink(), key.encodeUtf8()))

private fun File.calculateHash(hashingSink: HashingSink) =
    hashingSink.use {
        source().buffer().use { source ->
            source.readAll(hashingSink)
            hashingSink.hash.hex()
        }
    }


/** @link https://en.wikipedia.org/wiki/List_of_file_signatures */
val magics =
    mapOf(
        "ffd8ff" to "jpg",
        "60ea" to "arj",
        "785634" to "pbt",
        "aced" to "Serialized Java Data",
        "0000000c6a5020200d0a" to "jp2",
        "89504e47" to "png",
        "474946383761" to "gif(87a)",
        "474946383961" to "gif(89a)",
        "0001000000" to "ttf",
        "49492a00227105008037" to "tif",
        "424d" to "bmp",
        "edabeedb" to "rpm",
        "41433130313500000000" to "dwg",
        "7b5c727466315c616e73" to "rtf",
        "38425053000100000000" to "psd",
        "46726f6d3a203d3f6762" to "eml",
        "5374616e64617264204a" to "mdb",
        "252150532d41646f6265" to "ps",
        "255044462d312e" to "pdf",
        "2e524d46000000120001" to "rmvb",
        "464c56" to "flv",
        "0000001c66747970" to "mp4",
        "00000020667479706" to "mp4",
        "00000018667479706d70" to "mp4",
        "494433" to "mp3",
        "000001ba210001000180" to "mpg",
        "3026b2758e66cf11a6d9" to "wmv",
        "52494646e27807005741" to "wav",
        "664c6143" to "flac",
        "52494646d07d60074156" to "avi",
        "4d546864000000060001" to "mid",
        "526172211a07" to "rar",
        "235468697320636f6e66" to "ini",
        "504b03040a0000080800" to "jar",
        "504b03040a0000000000" to "xlsx",
        "504b03040a0000080000" to "jar",
        "504b0304140008000800" to "jar",
        "504b0304140008080800" to "jar",
        "504b03040a0008080800" to "jar",
        "d0cf11e0a1b11ae1" to "xls",
        "d0cf11e0a1b11ae1" to "xlsx",
        "504b0304" to "zip",
        "4d5a9000030000000400" to "exe",
        "7f454c46" to "elf",
        "3c25402070616765206c" to "jsp",
        "4d616e69666573742d56" to "mf",
        "7061636b616765207765" to "java",
        "406563686f206f66660d" to "bat",
        "1f8b" to "gz",
        "cafebabe00000031" to "class(49,jdk1.5)",
        "cafebabe00000032" to "class(50,jdk1.6)",
        "cafebabe00000033" to "class(51,jdk1.7)",
        "cafebabe00000034" to "class(52,jdk1.8)",
        "cafebabe00000035" to "class(53,jdk9)",
        "cafebabe00000036" to "class(54,jdk10)",
        "cafebabe00000037" to "class(55,jdk11)",
        "cafebabe00000038" to "class(56,jdk12)",
        "cafebabe00000039" to "class(57,jdk13)",
        "cafebabe0000003a" to "class(58,jdk14)",
        "cafebabe0000003b" to "class(59,jdk15)",
        "cafebabe0000003c" to "class(60,jdk16)",
        "cafebabe0000003d" to "class(61,jdk17)",
        "cafebabe0000003e" to "class(62,jdk18)",
        "cafebabe0000003f" to "class(63,jdk19)",
        "cafebabe00000040" to "class(64,jdk20)",
        "cafebabe00000041" to "class(65,jdk21)",
        "cafebabe00000042" to "class(66,jdk22)",
        "cafebabe00000043" to "class(67,jdk23)",
        "cafebabe00000044" to "class(68,jdk24)",
        "cafebabe00000045" to "class(69,jdk25)",
        "cafebabe00000046" to "class(70,jdk26)",
        "cafebabe00000047" to "class(71,jdk27)",
        "49545346030000006000" to "chm",
        "04000000010000001300" to "mxp",
        "6431303a637265617465" to "torrent",
        "6d6f6f76" to "mov",
        "ff575043" to "wpd",
        "cfad12fec5fd746f" to "dbx",
        "2142444e" to "pst",
        "ac9ebd8f" to "qdf",
        "e3828596" to "pwl",
        "2e7261fd" to "ram",
        "00000100" to "ico",
        "6465780a30333500" to "dex",
        "377abcaf271c" to "7z",
        "04224d18" to "lz4",
        "4c5a4950" to "lz",
        "3c3f786d6c20" to "xml",
        "4c0000000" to "lnk",
        "feedfeed" to "jks",
        "52494646" to "webp",
        "1b4c7561" to "luac",
        "d4c3b2a1" to "pcap",
        "a1b2c3d4" to "pcap",
        "4d3cb2a1" to "pcap",
        "a1b23c4d" to "pcap",
        "0a0d0d0a" to "pcapng",
        "2321" to "#! shell",
        "774f4646" to "woff",
        "774f4632" to "woff2",
        "0061736d" to "wasm",
        "4f54544f" to "otf",
        "2321414d52" to "amr",
        "2e736e64" to "snd"
    )

val multiExts = listOf("zip", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "jar", "apk")

val unsupportedExts = (magics.values + multiExts).toSet()

fun File.realExtension() =
    if (isFile) {
        if (extension.lowercase() == "txt") {
            "txt"
        } else {
            magicNumber().run {
                magics.keys
                    .firstOrNull { it.startsWith(it, true) }
                    ?.let { key ->
                        println(name + " magic: $this " + key + " " + magics[key])
                        (if (magics[key] in multiExts) {
                            "$extension(probably)".takeIf { extension != name } ?: magics[key]
                        } else {
                            magics[key]
                        })
                    }
                    ?: "$extension(probably)".also { println("unknown magic number $this $name") }
            }
        }
    } else {
        "dir"
    }

@OptIn(ExperimentalStdlibApi::class)
fun File.magicNumber(bytes: Int = 10) =
    inputStream().use {
        val b = ByteArray(bytes)
        it.read(b)
        b.toHexString()
    }

fun File.toBase64() = readBytes().encodeBase64ToString()

/** 读取文件内容,限制大小 */
fun File.properText(limit: Int = 128 * 1024, hints: String = "") =
    if (length() <= limit) {
        val ext = realExtension()
        if (ext in unsupportedExts) {
            "unsupported extension $ext"
        } else {
            readText()
        }
    } else {
        "not support file larger than ${hints.ifEmpty { "128KB" }}, extension ${realExtension()}"
    }

fun String.toFile() = File(this)

/*
inline fun <reified T> readRes(path: String) =
    T::class.java.getResourceAsStream(path)?.reader()?.readText().orEmpty()
*/


/**
 * Helper function for obtaining and holding a file lock until the given lambda completes
 *
 * @param shared Flag to indicate if the lock is shared. Defaults to false.
 */
inline fun <T> File.withFileLock(shared: Boolean = false, block: () -> T): T? {
    return synchronized(this) {
        var randomAccessFile: RandomAccessFile? = null
        try {
            randomAccessFile = RandomAccessFile(this, if (shared) "r" else "rw")
            return@synchronized randomAccessFile.withFileLock(shared, block)
        } catch (e: IOException) {
            Timber.w("IOException while obtaining file lock", e)
        } catch (e: Error) {
            Timber.w("Error while obtaining file lock", e)
        } finally {
            // The object may have a bad file descriptor, but if we don't call "close()", this will lead to a crash when GC cleans this object up
            try {
                randomAccessFile?.close()
            } catch (e: IOException) {
                Timber.w( "Exception thrown while closing the RandomAccessFile", e)
            }
        }
        return@synchronized null
    }
}

@Throws(IOException::class)
@JvmSynthetic
@PublishedApi
internal inline fun <T> RandomAccessFile.withFileLock(shared: Boolean, block: () -> T): T {
    // Lock the file to prevent other process from writing to it while this read is occurring. This is reentrant
    var lock: FileLock? = null
    try {
        // File Channel must be write enabled for exclusive file locking
        // This should block the thread, and prevent misuse of CPU cycles
        lock = channel.lock(0L, Long.MAX_VALUE, shared)
        return block()
    } finally {
        try {
            lock?.release()
        } catch (e: IOException) {
            throw IOException("Unable to release FileLock!", e)
        }
    }
}

/**
 * Helper function for performing an fsync() on the given [FileOutputStream]
 */
/*fun FileOutputStream.sync() {
    Os.fsync(fd)
}*/
